import sys
import asyncio
import threading
from PyQt5.QtWidgets import (
    QApplication, QWidget, QPushButton, QTextEdit, QVBoxLayout,
    QListWidget, QListWidgetItem, QTreeWidget, QTreeWidgetItem,
    QLabel, QInputDialog, QMessageBox
)
from PyQt5.QtCore import pyqtSignal, QObject
from bleak import BleakScanner, BleakClient


class BleWorker(QObject):
    result_ready = pyqtSignal(str)
    device_list_ready = pyqtSignal(list)
    service_list_ready = pyqtSignal(list)
    notify_received = pyqtSignal(str)

    def __init__(self, loop):
        super().__init__()
        self.loop = loop
        self.client = None
        self.devices = []
        self.char_properties = {}
        self.notify_handlers = {}

    def start_scan(self):
        asyncio.run_coroutine_threadsafe(self.scan(), self.loop)

    async def scan(self):
        self.result_ready.emit("🔍 开始扫描...")
        try:
            self.devices = await BleakScanner.discover()
            if self.devices:
                self.device_list_ready.emit(self.devices)
                self.result_ready.emit(f"✅ 共发现 {len(self.devices)} 个设备")
            else:
                self.result_ready.emit("⚠️ 未发现设备")
        except Exception as e:
            self.result_ready.emit(f"❌ 扫描失败: {e}")

    def connect_device(self, address):
        asyncio.run_coroutine_threadsafe(self._connect(address), self.loop)

    async def _connect(self, address):
        try:
            self.client = BleakClient(address)
            await self.client.connect()
            if self.client.is_connected:
                self.result_ready.emit(f"✅ 成功连接到设备 {address}")
                services = self.client.services
                svc_info = []
                self.char_properties.clear()
                for svc in services:
                    chars = []
                    for char in svc.characteristics:
                        uuid = str(char.uuid)
                        props = char.properties
                        self.char_properties[uuid] = props
                        chars.append({"uuid": uuid, "properties": props})
                    svc_info.append({"uuid": str(svc.uuid), "chars": chars})
                self.service_list_ready.emit(svc_info)
            else:
                self.result_ready.emit("❌ 连接失败")
        except Exception as e:
            self.result_ready.emit(f"❌ 连接错误: {e}")

    def read_characteristic(self, uuid):
        asyncio.run_coroutine_threadsafe(self._read_char(uuid), self.loop)

    async def _read_char(self, uuid):
        try:
            value = await self.client.read_gatt_char(uuid)
            self.result_ready.emit(f"📥 读取 {uuid}:\nHEX: {value.hex()}\nASCII: {value.decode(errors='ignore')}")
        except Exception as e:
            self.result_ready.emit(f"❌ 读取失败: {e}")

    def write_characteristic(self, uuid, value_bytes):
        asyncio.run_coroutine_threadsafe(self._write_char(uuid, value_bytes), self.loop)

    async def _write_char(self, uuid, value_bytes):
        try:
            await self.client.write_gatt_char(uuid, value_bytes)
            self.result_ready.emit(f"📤 写入成功 UUID: {uuid}")
        except Exception as e:
            self.result_ready.emit(f"❌ 写入失败: {e}")

    def toggle_notify(self, uuid):
        if uuid in self.notify_handlers:
            asyncio.run_coroutine_threadsafe(self._stop_notify(uuid), self.loop)
        else:
            asyncio.run_coroutine_threadsafe(self._start_notify(uuid), self.loop)

    async def _start_notify(self, uuid):
        try:
            def callback(sender, data):
                msg = f"🔔 通知 [{sender}]: {data.hex()} / {data.decode(errors='ignore')}"
                self.notify_received.emit(msg)

            await self.client.start_notify(uuid, callback)
            self.notify_handlers[uuid] = callback
            self.result_ready.emit(f"📡 开始订阅通知 UUID: {uuid}")
        except Exception as e:
            self.result_ready.emit(f"❌ 订阅失败: {e}")

    async def _stop_notify(self, uuid):
        try:
            await self.client.stop_notify(uuid)
            del self.notify_handlers[uuid]
            self.result_ready.emit(f"🛑 已取消订阅通知 UUID: {uuid}")
        except Exception as e:
            self.result_ready.emit(f"❌ 取消订阅失败: {e}")


class MainWindow(QWidget):
    def __init__(self, loop):
        super().__init__()
        self.setWindowTitle("💡 BLE 工具 - bleak + PyQt5")
        self.resize(600, 700)
        self.loop = loop
        self.worker = BleWorker(loop)

        self.output = QTextEdit()
        self.output.setReadOnly(True)
        self.btn_scan = QPushButton("🔍 扫描 BLE 设备")
        self.device_list = QListWidget()
        self.label_services = QLabel("📑 服务与特征：")
        self.service_tree = QTreeWidget()
        self.service_tree.setHeaderLabels(["UUID", "类型 / 属性"])

        layout = QVBoxLayout()
        layout.addWidget(self.btn_scan)
        layout.addWidget(self.device_list)
        layout.addWidget(self.label_services)
        layout.addWidget(self.service_tree)
        layout.addWidget(self.output)
        self.setLayout(layout)

        self.btn_scan.clicked.connect(self.worker.start_scan)
        self.device_list.itemClicked.connect(self.on_device_selected)
        self.service_tree.itemDoubleClicked.connect(self.on_characteristic_clicked)

        self.worker.result_ready.connect(self.output.append)
        self.worker.device_list_ready.connect(self.update_device_list)
        self.worker.service_list_ready.connect(self.update_services)
        self.worker.notify_received.connect(self.output.append)

    def update_device_list(self, devices):
        self.device_list.clear()
        for dev in devices:
            item = QListWidgetItem(f"{dev.name or 'Unknown'} ({dev.address})")
            item.setData(1000, dev.address)
            self.device_list.addItem(item)

    def on_device_selected(self, item):
        address = item.data(1000)
        self.output.append(f"🔗 正在连接 {address}...")
        self.worker.connect_device(address)

    def update_services(self, services):
        self.service_tree.clear()
        for svc in services:
            svc_item = QTreeWidgetItem([svc["uuid"], "Service"])
            for char in svc["chars"]:
                props = ", ".join(char["properties"])
                char_item = QTreeWidgetItem([char["uuid"], f"Char [{props}]"])
                char_item.setData(1000, char["uuid"])
                svc_item.addChild(char_item)
            self.service_tree.addTopLevelItem(svc_item)

    def on_characteristic_clicked(self, item, column):
        uuid = item.data(1000)
        if not uuid:
            return

        props = self.worker.char_properties.get(uuid, [])
        self.output.append(f"📌 选择特征 UUID: {uuid} (属性: {props})")

        if "read" in props:
            self.worker.read_characteristic(uuid)
        if "write" in props or "write-without-response" in props:
            text, ok = QInputDialog.getText(self, "写入特征", f"输入写入 {uuid} 的内容（hex 或 ASCII）：")
            if ok:
                try:
                    if all(c in "0123456789abcdefABCDEF " for c in text.strip()):
                        value_bytes = bytes.fromhex(text.strip())
                    else:
                        value_bytes = text.encode()
                    self.worker.write_characteristic(uuid, value_bytes)
                except Exception as e:
                    QMessageBox.warning(self, "格式错误", f"解析失败: {e}")
        if "notify" in props:
            result = QMessageBox.question(self, "订阅通知", f"要 {'取消' if uuid in self.worker.notify_handlers else '开始'} 订阅 {uuid} 的通知吗？")
            if result == QMessageBox.Yes:
                self.worker.toggle_notify(uuid)


def start_loop(loop):
    asyncio.set_event_loop(loop)
    loop.run_forever()

def main():
    app = QApplication(sys.argv)
    loop = asyncio.new_event_loop()
    threading.Thread(target=start_loop, args=(loop,), daemon=True).start()
    window = MainWindow(loop)
    window.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()
